package com.googlecode.alogwe.sprockit.engine;

import java.io.FileNotFoundException;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.mozilla.javascript.commonjs.module.*;
import org.mozilla.javascript.commonjs.module.provider.*;

import com.googlecode.alogwe.sprockit.js.JSScript;

public class JSEngine extends ScriptableObject implements Runnable {

	private ExecutorService		executor;
	private static ScriptQueue	loadingQueue;
	private boolean				terminationRequested;

	/**
	 * The zero-parameter public constructor, required by Rhino for all host
	 * objects
	 * <p>
	 * When Context.defineClass is called with this class, it will construct
	 * Engine.prototype using this constructor.
	 */
	public JSEngine() {
		this.executor = Executors.newCachedThreadPool();
		JSEngine.loadingQueue = ScriptQueue.getInstance();
		this.terminationRequested = false;
		initializeModuleProvider();
		JSEngine.instance = this;
	}

	/**
	 * The Java method defining the JavaScript constructor.
	 */
	public void jsConstructor() {
	}

	private static JSEngine	instance	= null;

	/**
	 * Provides access to the singleton instance of this class.
	 * 
	 * @return The singleton instance of JSEngine.
	 */
	public static JSEngine getInstance() {
		if (JSEngine.instance == null) {
			JSEngine.instance = new JSEngine();
		}
		return JSEngine.instance;
	}

	private static final String	JS_CLASSNAME	= "Engine";

	/**
	 * Returns the name of this JavaScript class. Typically the same name as the
	 * constructor.
	 */
	@Override
	public String getClassName() {
		return JS_CLASSNAME;
	}

	/**
	 * Creates it's own JavaScript host object in the given context/scope
	 * <p>
	 * <b>TODO</b> Figure out a better way to do this.
	 * 
	 * @param cx
	 *            The Context used to create the JavaScript host object
	 * @param scope
	 *            The object in the JavaScript environment that will receive
	 *            this newly created host object
	 * @throws IllegalAccessException
	 * @throws InstantiationException
	 * @throws InvocationTargetException
	 */
	public static void createJsHostObject(Context cx, Scriptable scope)
			throws IllegalAccessException, InstantiationException, InvocationTargetException {
		ScriptableObject.defineClass(scope, JSEngine.class);
		scope.put(JS_CLASSNAME, scope, getInstance());
	}

	public static Require createJsRequire(Context cx, Scriptable topLevelScope) {
		Require require = JSEngine.requireBuilder.createRequire(cx, topLevelScope);
		return require;
	}

	private static ModuleSourceProvider	sourceProvider;
	private static ModuleScriptProvider	scriptProvider;
	private static RequireBuilder		requireBuilder;

	/*
	 * TODO Consider separating/isolating the module providing features
	 */
	private void initializeModuleProvider() {
		URI applicationBaseDir = Paths.get("").toUri();
		List<URI> moduleBaseLocations = Arrays.asList(applicationBaseDir);

		JSEngine.sourceProvider = new UrlModuleSourceProvider(moduleBaseLocations, null);
		JSEngine.scriptProvider = new SoftCachingModuleScriptProvider(sourceProvider);

		JSEngine.requireBuilder = new RequireBuilder();
		JSEngine.requireBuilder.setModuleScriptProvider(scriptProvider);
	}

	/**
	 * Defines the JavaScript host object function:
	 * <p>
	 * <code>Engine.load(sFilepath, <i>sFunction</i>)</code><br>
	 * <b>sFilepath</b> is a JavaScript String representing the relative path
	 * and filename<br>
	 * <b>sFunction (<i>optional</i>)</b> is a JavaScript String which names the
	 * Function object to be called in the newly threaded script. If not
	 * provided, sFunction will default to "main", resulting in a call to<br>
	 * <code>function main() {...}</code>
	 * <p>
	 * <b>TODO</b> Have this return a "Script" object. <br>
	 * <p>
	 * All variable arguments host object methods must have the same number and
	 * types of parameters, and must be static.
	 * <p>
	 * 
	 * @param cx
	 *            the Context of the current thread
	 * @param thisObj
	 *            the JavaScript 'this' value.
	 * @param args
	 *            the array of arguments for this call
	 * @param funObj
	 *            the function object of the invoked JavaScript function This
	 *            value is useful to compute a scope using
	 *            Context.getTopLevelScope().
	 * @return void
	 * @see JSEngine#load(String, String)
	 * @see <a
	 *      href=http://mxr.mozilla.org/mozilla/source/js/rhino/examples/Foo.java
	 *      > Variable argument JavaScript binding.</a>
	 * @see org.mozilla.javascript.ScriptableObject#getTopLevelScope
	 * @throws org.mozilla.javascript.WrappedException
	 *             Thrown in the JavaScript runtime
	 */
	public static void jsFunction_load(Context cx, Scriptable thisObj, Object[] args,
			Function funObj) {

		String filepath = "";
		String function = "main"; // Default function name for loaded script;
		int len = args.length;

		if (len == 0) {
			throw Context.throwAsScriptRuntimeEx(new Exception(
					"At least one parameter is required. (String filepath)"));
		}

		if (len >= 1) {
			Object sFilepath = args[0];
			if (!(sFilepath instanceof String)) {
				throw Context.throwAsScriptRuntimeEx(new Exception(
						"First argument provided must be a String"));
			}
			filepath = Context.toString(sFilepath);
		}

		if (len >= 2) {
			Object sFunction = args[1];
			if (!(sFunction instanceof String)) {
				throw Context.throwAsScriptRuntimeEx(new Exception(
						"Second argument provided must be a String"));
			}
			function = Context.toString(sFunction);
		}

		try {
			JSEngine.load(filepath, function);
		} catch (FileNotFoundException e) {
			// Re-throw wrapped FileNotFoundException into script runtime
			throw Context.throwAsScriptRuntimeEx(e);
		}
	}

	/**
	 * The private Java method that backs the Engine.load() JavaScript function.
	 * <p>
	 * 
	 * @param filepath
	 *            The relative local path to the JavaScript script file.<br>
	 * @param function
	 *            The name of the main JavaScript function which will
	 *            automatically be called upon loading.
	 *            <p>
	 * @throws FileNotFoundException
	 * @see JSEngine#jsFunction_load()
	 */
	private static void load(String filepath, String function) throws FileNotFoundException {
		try {
			JSScript script = JSScript.createNew(filepath, function);

			// Producing task
			loadingQueue.put(script);

			print("Yielding in load().");
			Thread.yield();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	/**
	 * Prints to System.out with thread ID number. Used for debugging purposes.
	 * 
	 * @param output
	 *            String to be written.
	 */
	private static void print(String output) {
		Thread t = Thread.currentThread();
		String threadInfo = "Thread [" + t.getId() + "] : ";
		System.out.println(threadInfo + output);
	}

	@Override
	public void run() {
		try {

			JSEngine.load("scripts/sprockit.js", "main");

			while (!terminationRequested) {

				// Consuming task
				JSScript nextScript = loadingQueue.get();

				String filepath = nextScript.getFile().getPath();
				print("Passing script [\"" + filepath + "\"] to Executor");
				executor.execute(nextScript);
				print("Yielding in JSEngine run()");
				Thread.yield();
				print("Terminate Engine? " + terminationRequested);
			}

			/*
			 * TODO Thread termination / Cleanup / Shutdown.
			 */

		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

}
